Making an Application¶
No more theory, let us now get into the business of making applications. In this tutorial we will make a very basic application that helps you in understanding the logics. We have divided the tutorial into simpler parts so it is easy to follow.
Since the application should be easy, we will handle only a very few
widgets of Gtk
. The details of the widgets that are used in our providers
are given below :
- An entry widget takes single line input from user. It can also be used to display text that can only be copied but not modified. When the text in an entry is changed, it emits changed signal. To prevent editing the text of an entry, we set its editable property to False.
- To open, save, select files or folders we need a file chooser widget. After user selects a file in the file chooser dialog, a file-set signal is emitted.
- A grid allows packing widgets into a single widget in a tabular format. To attach a widget to grid, we use the gird’s
Gtk.Grid.attach()
method. - Text can be displayed using a label widget. In our demo application, we are using only basic features of a label, that is just display text.
- Menu is often a linear list of textual buttons. To break that rule and include other widgets in menu, we use model buttons.
- Popovers are used to display something for a short period of time. They are usually used for drop-down menus.
- If a widget is too large to be accommodated in given space, we use a scrolled window. This shows only the portion of the widget that could be displayed and rest can be scrolled.
- Text buffer is used to store the text for a text view widget. A single text buffer can be shared across multiple text views.
- To enable multi-line text compatibility a text view widget is used. Text view is a front-end and the text in it is controlled using a text buffer. As in the case of label, we are not going to use the full power of a text view.
- A toggle button is like a switch, it can have two states active and inactive. A toggled signal is emitted if the state of the button is changed.
- A window is the top-level widget that represents your application. We quit Gtk’s main loop when the main window is destroyed.
So let’s get started.
Making Providers¶
Providers are the core part of our application. Because of the design philosophy of Aduct, every part of an application behaves like a plugin. This makes an application modular in every way. To keep things simple, we make only three providers.
- Provider AIt gives a text entry and a toggle button. The toggle button allows editing the entry.
- Provider BIt gives a text view and a file chooser button. The file chooser button opens the file for displaying.
- Provider CIt gives a label with text Hello World.
Note
If the code to make the first two providers are difficult, then copy the code for Provider C and change the text for label, but the results will vary.
To make providers, we inherit Aduct.Provider
. Then we add the required
methods (please refer Aduct.Provider
for more details on required
methods).
In Aduct, you will often see variables named child_dict
. A child_dict
is of the following format.
child_dict = {
"child": Gtk.Widget,
"child_name": str,
"icon": Gtk.Image,
"header_child": Gtk.Widget or None,
"provider": Aduct.Provider
}
The source code for our providers are as follows. If you are lazy to copy-paste,
download
it.
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GObject
import Aduct
class Provider_A(Aduct.Provider):
name = GObject.Property(type=str, default="Provider A", flags=GObject.ParamFlags.READABLE)
def __init__(self):
Aduct.Provider.__init__(self)
self.text = ""
self.toggles = []
self.entries = []
self.editable = False
def change_text(self, entry):
self.text = entry.get_text()
for entry in self.entries:
entry.set_text(self.text)
def clear_child(self, child_props):
self.entries.remove(child_props["child"])
self.toggles.remove(child_props["header_child"])
del child_props
def get_a_child(self, child_name):
entry = Gtk.Entry(margin=5, text=self.text, editable=self.editable)
entry.connect("changed", self.change_text)
icon = Gtk.Image.new_from_icon_name("terminal", 2)
# Choose whatever icon you want
switch = Gtk.ToggleButton(
label="Allow Edit", hexpand=True, halign=2, active=self.editable
)
switch.connect("toggled", self.toggle_editable)
self.entries.append(entry)
self.toggles.append(switch)
child_props = {
"child_name": "Entry",
"child": entry,
"icon": icon,
"header_child": switch,
}
return child_props
def get_child_props(self, child_name, child, header_child):
props = {"child_name": child_name, "text": self.text, "editable": self.editable}
return props
def get_child_from_props(self, props):
self.editable = props["editable"]
self.text = props["text"]
for toggle in self.toggles:
toggle.set_active(self.editable)
for entry in self.entries:
entry.set_editable(self.editable)
entry.set_text(self.text)
return self.get_a_child(props["child_name"])
def toggle_editable(self, toggle):
self.editable = toggle.get_active()
for toggle in self.toggles:
toggle.set_active(self.editable)
for entry in self.entries:
entry.set_editable(self.editable)
class Provider_B(Aduct.Provider):
name = GObject.Property(type=str, default="Provider B", flags=GObject.ParamFlags.READABLE)
def __init__(self):
Aduct.Provider.__init__(self)
self.file_choosers = []
self.buffer = Gtk.TextBuffer()
self.path = None
def clear_child(self, child_props):
self.file_choosers.remove(child_props["header_child"])
del child_props
def change_text_at_buffer(self, fp_but):
path = fp_but.get_filename()
self.path = path
fp = open(path)
text = fp.read()
self.buffer.set_text(text)
fp.close()
for fp_chooser in self.file_choosers:
fp_chooser.set_filename(self.path)
def get_a_child(self, child_name):
textview = Gtk.TextView(margin=5, buffer=self.buffer)
scrolled = Gtk.ScrolledWindow(expand=True)
scrolled.add(textview)
icon = Gtk.Image.new_from_icon_name("folder", 2)
fp_but = Gtk.FileChooserButton(title="Choose file", hexpand=True, halign=2)
if self.path:
fp_but.set_filename(self.path)
fp_but.connect("file-set", self.change_text_at_buffer)
self.file_choosers.append(fp_but)
child_props = {
"child_name": "TextView",
"child": scrolled,
"icon": icon,
"header_child": fp_but,
}
return child_props
def get_child_props(self, child_name, child, header_child):
props = {"child_name": child_name, "path": self.path}
return props
def get_child_from_props(self, props):
self.path = props["path"]
if self.path:
fp = open(self.path)
text = fp.read()
self.buffer.set_text(text)
fp.close()
for fp_chooser in self.file_choosers:
fp_chooser.set_filename(self.path)
return self.get_a_child(props["child_name"])
class Provider_C(Aduct.Provider):
name = GObject.Property(type=str, default="Provider C", flags=GObject.ParamFlags.READABLE)
def __init__(self):
Aduct.Provider.__init__(self)
def clear_child(self, child_props):
del child_props
def get_a_child(self, child_name):
label = Gtk.Label(margin=5, label="Hello world")
icon = Gtk.Image.new_from_icon_name("glade", 2)
child_props = {
"child_name": "Label",
"child": label,
"icon": icon,
"header_child": None,
}
return child_props
def get_child_props(self, child_name, child, header_child):
props = {"child_name": child_name}
return props
def get_child_from_props(self, props):
return self.get_a_child(props["child_name"])
A = Provider_A()
B = Provider_B()
C = Provider_C()
We prefer keeping the above code in a separate file (could be named providers.py), because in practical situations (while making real applications) it is better to isolate providers from the core application, this makes it easy to maintain. The reason we imported Gtk from Aduct is not so crucial. It was done to reduce typing, also it makes sure that we are using the same version of Gtk that Aduct is using.
Designing the Application¶
Now we have made providers, our next step is to frame the application. Open a new file (could be named app.py). To allow widgets in the application, we should put a way for users to view available widgets and select the required. This is done using action button of element and notebook. The convention is when users left-click an action button, it should show widgets (from providers) and when they right-click, it should show options to modify the interface.
There are two suitable ways to show the users the widgets to select from. It could be a popup window. But pop-ups are considered distracting. The second option is drop-down menu (also known as popover menu). Popovers are better as they cover only a small area and are not as annoying as popup windows. We populate the menu with model buttons.
Before adding providers, we should also spend time in a kind of
functions known as creator functions. As Aduct is an interface to
dynamically modify an interface, you need to be able to dynamically make
Aduct widgets. So we make small functions that, when called give the
required widget. An advantage of such functions is that they can be used
to add custom changes to widgets like changing border spacing,
connecting signals and automate other repeating tasks. The first few
lines of app.py is given below. (The complete file is also available for
download
.)
import Aduct
from Aduct import Gtk
from providers import A, B, C # providers from provider.py
last_widget = None # The last widget (element/notebook) where popover was shown.
def new_element():
element = Aduct.Element(margin=5)
element.connect("action-clicked", show_popover_element)
# show_popover_element is a function to show the popover for an element.
return element
def new_bin():
bin_ = Aduct.Bin()
return bin_
def new_paned(orientation=0):
paned = Aduct.Paned(orientation=orientation)
return paned
def new_notebook():
notebook = Aduct.Notebook()
button = Gtk.Button()
icon = Gtk.Image.new_from_icon_name("list-add", 2)
button.add(icon)
notebook.set_action_button(button, 1)
notebook.connect("action-clicked", show_popover_notebook)
# show_popover_notebook is a function like show_popover_element.
return notebook
The idea of the above code is simple. When action button of an element
or notebook is clicked, it emits a signal and popover is shown in
return. The popover contains model buttons for various purposes, when
they are clicked, they need to know for which element or notebook they
were clicked. To tackle this, when an action button is clicked, we
correspondingly set the value of last_widget
to that widget. With
that, let’s append the next lines of code.
def show_popover_element(ele, but, event):
global last_widget
last_widget = ele
if event == 1: # 1 -> left-click of mouse
prov_popover.set_relative_to(but)
prov_popover.popup()
elif event == 3: # 3 -> right-click of mouse
for modbs in tweaks.values():
for modb in modbs:
modb.set_sensitive(True)
tweak_popover.set_relative_to(but)
tweak_popover.popup()
def show_popover_notebook(nb, but, event):
global last_widget
last_widget = nb
if event == 1:
prov_popover.set_relative_to(but)
prov_popover.popup()
elif event == 3:
for modb in tweaks["Element"]:
modb.set_sensitive(False)
for modb in tweaks["Notebook"]:
modb.set_sensitive(False)
tweak_popover.set_relative_to(but)
tweak_popover.popup()
Both the above functions are same but the difference between them is
that first one is for an element and second is for a notebook.
prov_popover
is for displaying widgets from providers and
tweak_popover
is for showing options to modify the interface. As per
the convention mentioned earlier, we show prov_popover
when users
left-click and tweak_popover
when users right-click an action button. We will cover
later why we are changing sensitivities of model buttons.
Now let us make some more functions that can modify the interface.
def remove_element(wid):
global last_widget
Aduct.remove_element(last_widget, last_widget.get_parent())
def add_to_paned(wid, position):
global last_widget
element = new_element()
paned = new_paned()
if position == 0:
paned.set_orientation(0)
Aduct.add_to_paned(last_widget, element, paned, 1)
elif position == 1:
paned.set_orientation(0)
Aduct.add_to_paned(last_widget, element, paned, 2)
elif position == 2:
paned.set_orientation(1)
Aduct.add_to_paned(last_widget, element, paned, 1)
elif position == 3:
paned.set_orientation(1)
Aduct.add_to_paned(last_widget, element, paned, 2)
def add_to_notebook(wid, position):
global last_widget
notebook = new_notebook()
notebook.set_tab_pos(position)
Aduct.add_to_notebook(last_widget, notebook)
Please read Functions to know the details of the functions used from Aduct. Next we add more functions for changing child at an element, saving and loading interfaces.
def change_child_at_element(wid, prov, child_name):
global last_widget
if last_widget.get_type() == "element":
Aduct.change_child_at_element(last_widget, prov, child_name)
elif last_widget.get_type() == "notebook":
element = new_element()
Aduct.change_child_at_element(element, prov, child_name)
Aduct.add_to_notebook(element, last_widget)
element.show_all()
def save_interface(wid):
from json import dump
with open("aduct.ui", "w") as fp:
ui_dict = Aduct.get_interface(top_level)
dump(ui_dict, fp, indent=2)
def load_interface(wid):
from json import load
with open("aduct.ui") as fp:
ui_dict = load(fp)
creator_maps = {
"type": {
"element": (new_element, (), {}),
"bin": (new_bin, (), {}),
"notebook": (new_notebook, (), {}),
"paned": (new_paned, (), {}),
}
}
init_maps = {
"provider": {"Provider A": A, "Provider B": B, "Provider C": C, None: None}
}
Aduct.set_interface(ui_dict, top_level, creator_maps, init_maps)
The first function does some straight-forward tasks. It changes a child at element when called from element. In case it is called from a notebook, we make a new element, get a child from provider and add it to the element. Then we append the element to the notebook.
The second function gets the interface; it is a dictionary with strings,
numbers and None. So it can be dumped using json in human-readable
format. We are using a file named aduct.ui for saving and loading
interfaces. top_level
(declared later) is the view or element from
which the interface should be fetched. It is usually the root widget.
The third function surely deserves a mention. After completing this tutorial, you can run the script (app.py), try playing with the interface. Then save the interface. After that open the file named aduct.ui, you will see a JSON-styled data with keys like type, provider etc. Now when you ask Aduct to create the interface from the same file, it replaces all the required values with objects (or widgets here). The convention is that key type states the type of Aduct widget and provider states the name of provider.
The dictionaries whose name ends with maps, does the job of replacing
strings or numbers with an object. They are nested-dictionaries
of depth two. It is like what key to replace? If found replace the value
of that key with the value from maps. For example, from init_maps
we
have the key provider. So first the set_interface
function will look
for any key named provider in ui_dict
. If found it will look at its
value, say it is Provider A, now it will go back to init_maps
and
look for the value of key Provider A in the dictionary which is the value
of key named provider. From the above it is provider A
, then the function
replaces the value Provider A in ui_dict
with the actual object;
provider A
. So you can consider it as a mapping of strings to
objects.
The purpose of creator_maps
and init_maps
are pretty same. Their
difference lies in values they are replacing. init_maps
maps to
object already created, that is initialized objects, like providers,
plugins, file objects. It is to replace object that are already
available and should not created again. On the other hand,
creator_maps
, dynamically creates objects as needed. It is for the
purpose where each object has to be unique or can be created multiple
times for multiple usage. The values of creator_maps
are of this
order (function, args, kwargs)
. While replacing
strings with objects, the object is created by calling the function like
this : object = function(*args, **kwargs)
.
Pretty simple as that, however if you are confused, remember them as mappings. That’s it.
The third function needs a root widget. It will remove whatever child it holds and replaces it with the children from the JSON file. However it returns the old child for recovering data, something not so necessary in our application.
The finishing parts of our application is just connecting everything, creating a new window and adding a top level view.
provs = [
(
A,
Gtk.ModelButton(text="Entry"),
"Entry",
), # Making a model-button for each provider.
(B, Gtk.ModelButton(text="TextView"), "TextView"),
(C, Gtk.ModelButton(text="Label"), "Label"),
]
prov_grid = Gtk.Grid() # A grid to store them
for y, (prov, modb, child_name) in enumerate(provs):
prov_grid.attach(modb, 0, y, 1, 1)
modb.connect("clicked", change_child_at_element, prov, child_name)
prov_popover = Gtk.PopoverMenu()
prov_popover.add(prov_grid)
prov_grid.show_all()
# Pretty same as providers, but for tweak functions.
tweaks = {
"Element": (Gtk.ModelButton(text="Remove"),),
"Notebook": (
Gtk.ModelButton(text="Add to top notebook"),
Gtk.ModelButton(text="Add to side notebook"),
),
"Paned": (
Gtk.ModelButton(text="Split left"),
Gtk.ModelButton(text="Split right"),
Gtk.ModelButton(text="Split up"),
Gtk.ModelButton(text="Split down"),
),
"Interface": (
Gtk.ModelButton(text="Load interface"),
Gtk.ModelButton(text="Save interface"),
),
}
tweak_grid = Gtk.Grid()
for x, title in enumerate(tweaks):
label = Gtk.Label(label=title)
tweak_grid.attach(label, x, 0, 1, 1)
modbs = tweaks[title]
for y, modb in enumerate(modbs):
tweak_grid.attach(modb, x, y + 1, 1, 1)
tweak_popover = Gtk.PopoverMenu()
tweak_popover.add(tweak_grid)
tweak_grid.show_all()
def connect_tweaks():
# Connecting model-buttons to required functions.
elem_modb = tweaks["Element"][0]
elem_modb.connect("clicked", remove_element)
top_nb_modb = tweaks["Notebook"][0]
top_nb_modb.connect("clicked", add_to_notebook, 2)
side_nb_modb = tweaks["Notebook"][1]
side_nb_modb.connect("clicked", add_to_notebook, 0)
l_paned_modb = tweaks["Paned"][0]
r_paned_modb = tweaks["Paned"][1]
u_paned_modb = tweaks["Paned"][2]
d_paned_modb = tweaks["Paned"][3]
# 0, 1, 2, 3 are integer values of Gtk.PositionType.
l_paned_modb.connect("clicked", add_to_paned, 0)
r_paned_modb.connect("clicked", add_to_paned, 1)
u_paned_modb.connect("clicked", add_to_paned, 2)
d_paned_modb.connect("clicked", add_to_paned, 3)
load_modb = tweaks["Interface"][0]
save_modb = tweaks["Interface"][1]
load_modb.connect("clicked", load_interface)
save_modb.connect("clicked", save_interface)
connect_tweaks()
top_level = new_bin()
element = new_element()
top_level.add_child(element) # Making a single element and adding it.
win = Gtk.Window(default_height=500, default_width=750)
win.add(top_level)
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()
Phew… we completed making the application! You might not have understood some parts, but still, run the application (run app.py) and see how it looks. Well just an empty screen with an empty button, right? Click on the action button of element and add new child to the element. Next try right clicking the action button to split it or add it to a notebook. Have fun removing the views and adding new one. When you are comfortable with the interface, save the interface and close the application. Now open it again and load the interface. You should see the interface you saved.
Let us discuss something we promised earlier. Run the application and add an element to notebook. Now right-click the element’s action button and click on Add to side notebook, you should see an error in your terminal or console that a Aduct notebook can only have a Aduct element as child. It is not a bug, it is a feature! Aduct notebook can only attach a named child and the only named child in Aduct is element. So you will get error when you try to add a child of irrelevant type to a notebook. This is the reason we changed the sensitivities of a few menu items. Because we don’t want to allow users to do something not permitted. We could have made separate popovers for notebook and element, but to avoid repeating codes with small difference, we omitted it. You might wonder why we didn’t change sensitivities of menu items of popovers shown for elements inside a notebook, the answer is we want you to try!
Note a few more things which might look absurd. When you remove an element from a paned, it collapses to a bin. When you remove an element from notebook with three or more pages, nothing goes wrong. But when you remove an element from a notebook with only two pages, the notebook drops to a bin. In case of a bin, when you try to remove its child element, instead of removing, it only clears the element.
This also has some reasons behind it. A paned is meant to hold two child, so when you remove its one child, the purpose of a paned is destroyed, so it becomes a bin. Similarly, a notebook is meant to hold a number of elements and show only one of them at a time. A notebook with only page is against its purpose, so it becomes a bin. For bin, the same logic is applied. A Aduct bin is, by convention, used as a top-level for holding other view. When you remove the element from it, the complete interface link is broken and you get a blank space, where no kind of interaction is possible. To avoid this, bin always clears the element instead of removing it.
However, if these behaviors are not acceptable to you, you are always free to create your own functions and use them.
While using the widgets like entry, text view in our application, you might have noticed that an entry is just a copy of another entry, with same text and mode, synchronized between them. This is to symbolize that widgets with same name are basically the same. But this is not enforced. It depends on the provider, it may produce a new widget or just a copy.
So here we reach the end of tutorial. There are some lines, paragraphs or entire section that doesn’t even make any sense to you. Feel free to discuss it with other developers to get help. Also if you think, the same matter could be presented in a better manner, you are always welcome to suggest your edits.
At last, we would like to say, designing an application is like painting. Everyone has a brush and seven basic colors. It depends on the painter how great he/she is going to make his/her art look. He/she may have a different ideology and style, its unique and can’t be duplicated.
Same in case of an application, Aduct is like brush and paint, it lies in your method, how well you are going to utilize it. Sometimes it could come out worse, where you should surely retry. Sometimes it could come great, where you should share the method with others (including us!). Also beauty lies in the eyes of the viewer, not in the painting… cheers!